module sqlite3d.Base;

import 
	sqlite3d.pub;
extern(C){
	char* sqlite3_sql(sqlite3_stmt*);
}

class SqliteException : Exception {
	static const char[] DescString	= "\nDesc: ";
	this(int code){
		super("Code:" ~  ctfe2_i2a(code));
	}
	this(char[] msg){
		super(DescString ~ msg);
	}
	this(int code, char[] msg){
		super("Code:" ~ ctfe2_i2a(code) ~ DescString ~ msg);
	}
	this(sqlite3* _pdb, int code, char[] msg = ""){
		if( _pdb !is null ){
			msg	~= "\nErrMsg: " ~ fromStringz(sqlite3_errmsg(_pdb));
		}
		super("Code:" ~  ctfe2_i2a(code) ~ DescString ~ msg);
	}
	this(sqlite3_stmt* _pstmt, int code, char[] msg = "" ){
		if( _pstmt !is null ){
			msg	~= "\nQuery String: " ~ fromStringz(sqlite3_sql(_pstmt));
		}
		super("Code:" ~  ctfe2_i2a(code) ~ DescString ~ msg);
	}
	this(sqlite3* _pdb, sqlite3_stmt* _pstmt, int code, char[] msg = "" ){
		if( _pdb !is null ){
			msg	~= "\nErrMsg: " ~ fromStringz(sqlite3_errmsg(_pdb));
		}
		if( _pstmt !is null ){
			msg	~= "\nQuery String: " ~ fromStringz(sqlite3_sql(_pstmt));
		}
		super("Code:" ~  ctfe2_i2a(code) ~ DescString ~ msg);
	}
}
alias SqliteException SqliteError;

struct Connection {
	sqlite3*		_pdb;

	delete(void* p) {
		sqlite3*	_pdb	=  cast(sqlite3*) *(cast(size_t*) p) ;
		if( _pdb !is null){
			int errcode = sqlite3_close(_pdb);
			if( errcode ){
				debug{
					//Stdout.formatln("-sqlite3 del {:x}:{}",  _pdb ,  *cast(int*)_pdb).flush;
				}
				throw new SqliteException(_pdb, errcode);
			}
		}
		delete p; 
	}
	
	void isErr(int code, lazy char[] msg = ""){
		if( code ){
			if( available ){
				throw new SqliteException(_pdb, code, msg );
			}else{
				throw new SqliteException(code, msg );
			}
		}
	}
	
	
	static Connection* open(char[] path) {
		Connection* conn	= new Connection;
		int errcode = sqlite3_open((path~"\0").ptr, &conn._pdb);
		debug {
			//Stdout.formatln("-sqlite3 new {:x}:{}", conn._pdb, * cast(int*) conn._pdb).flush;
		}
		conn.isErr(errcode, "Can't Open Connection");
		return conn;
	}
	
	void close() {
		if( available ){
			debug {
				//Stdout.formatln("-sqlite3 close {:x}:{}", _pdb, *cast(int*)_pdb).flush;
			}
			int errcode = sqlite3_close(_pdb);
			isErr(errcode,  "Connection Is Still Available, Close All Cursors And Statements Created By It And Try Again");
			_pdb	= null;
		}
	}
		
	bool isAutoCommit() {
		version (SQLITE_SAFE) _checkConnectionAvailable();
		return cast(bool) sqlite3_get_autocommit(_pdb);
	}
	
	void beginTransaction() {	/// Begin a transaction.
		version (SQLITE_SAFE) _checkConnectionAvailable();
		int errcode = sqlite3_exec(_pdb, "begin;", null, null, null);
		isErr(errcode);
	}
	alias beginTransaction begin;
	
	void commitTransaction() {  /// Commit current transaction.
		version (SQLITE_SAFE) _checkConnectionAvailable();
		int errcode = sqlite3_exec(_pdb, "commit;", null, null, null);
		isErr(errcode);
	}
	alias commitTransaction commit;
		
	void rollbackTransaction() {  /// Rollback current transaction.
		version (SQLITE_SAFE) _checkConnectionAvailable();
		int errcode = sqlite3_exec(_pdb, "rollback;", null, null, null);
		isErr(errcode);
	}
	alias rollbackTransaction rollback;
		
	int changes() {	/// Changes by last execution.
		version (SQLITE_SAFE) _checkConnectionAvailable();
		return sqlite3_changes(_pdb);
	}
	
	int totalChanges() { /// Total changes after connection opened.
		version (SQLITE_SAFE) _checkConnectionAvailable();
		return sqlite3_total_changes(_pdb);
	}
	
	long lastInsertRowId() { /// Last inserted rowid.
		version (SQLITE_SAFE) _checkConnectionAvailable();
		return sqlite3_last_insert_rowid(_pdb);
	}
	
	int getErrorCode() { /// Get last error code.
		version (SQLITE_SAFE) _checkConnectionAvailable();
		return sqlite3_errcode(_pdb);
	}

	char[] getErrorText() { /// Get last error description.
		version (SQLITE_SAFE) _checkConnectionAvailable();
		char* msg = sqlite3_errmsg(_pdb);
		return fromStringz(msg);
	}
	wchar[] getErrorText16() {
		version (SQLITE_SAFE) _checkConnectionAvailable();
		wchar* msg = cast(wchar*) sqlite3_errmsg16(_pdb);
		return fromString16z(msg);
	}
	
	bool available() {
		return _pdb !is null;
	}

	version (SQLITE_SAFE) {
		void _checkConnectionAvailable(){
			if( !available ){
				throw new SqliteError(-101);
			}
		}
	}
	
	public void exec(char[] sql) {
		int len	= sql.length;
		int code	= sqlite3_exec(_pdb, toStringz(sql), null, null, null);
		isErr(code);
		/*
			scope Statement* stmt	= Statement.Create(this);
			scope(exit){
				delete stmt;
			}
			int code;
			//Stdout.formatln("execute: {}", sql).flush;
			code	= stmt.prepare(sql);
			isErr(code, sql);
			stmt.step;
		*/
	}
}

struct Statement {
	sqlite3_stmt*	_pstmt	= null;
	private {
		Connection*	_conn;
		//char[]		pzTail	= null;
	}

	static Statement* Create(Connection* conn){
		Statement* stmt	= new Statement;
		stmt._conn		= conn;
		size_t* ptr		= cast(size_t*) stmt._pstmt;
		return stmt;
	}
	
	delete(void* p) {
		sqlite3_stmt*	_pstmt	=  cast(sqlite3_stmt*) *(cast(size_t*) p) ;
		if( _pstmt !is null ){
			int errcode = sqlite3_finalize(_pstmt);
			debug {
				//Stdout.formatln("-sqlite3_stmt del {:x}:{}", _pstmt, *cast(int*)_pstmt).flush;
			}
			if( errcode ){
				Connection* _conn	= cast(Connection*) *(cast(size_t*) p+1) ;
				if( _conn ){
					if( _conn.available ){
						throw new SqliteException(_conn._pdb, _pstmt, errcode);
					}
				}
				throw new SqliteException(_pstmt, errcode);
			}
		}
		delete p; 
	}
	
	bool available() {
		return _pstmt !is null;
	}
	
	version (SQLITE_SAFE) {
		void _checkStatementAvailable(){
			if( !available ){
				throw new SqliteError(-102);
			}
		}
	}
	
	int finalize() { // finalize statement and release resource.
		int errcode = sqlite3_finalize(_pstmt);
		if(!errcode){
			_pstmt	= null;
			//pzTail	= null;
		}
		return errcode;// OK/ABORT
	}
	alias finalize close;
	
	int step() { /// Step Statement execution, direct call on this is not recommended.
		version(SQLITE_SAFE) _checkStatementAvailable();
		return sqlite3_step(_pstmt); // DONE/ROW/BUSY/ERROR/MISUSE
	}
	
	int reset() { /// Reset Statement.
		version(SQLITE_SAFE) _checkStatementAvailable();
		return sqlite3_reset(_pstmt);
	}
	
	int prepare(char[] sql) {
		if( !_conn ){
			throw new SqliteException(-103);
		}
		if( !_conn.available){
			throw new SqliteException(-104);
		}
		if(available) finalize();
		return sqlite3_prepare_v2(_conn._pdb, sql.ptr, sql.length, &_pstmt, null);
	}
	
	void compile(char[] sql){
		int code = prepare(sql);
		isErr(code, "prepare `" ~ sql~"` failure");
	}

	bool isExpired() { /// Is Statement expired.
		if(!available) return true;
		return cast(bool) sqlite3_expired(_pstmt);
	}
	
	bool isSqlSelect() { /// Returns: True for a SQL SELECT statement.
		if(!available) return false;
		return cast(bool)(sqlite3_column_count(_pstmt) > 0);
	}
	
	int getColumnCount() { /// Get column count.
		version (SQLITE_SAFE) _checkStatementAvailable();
		return sqlite3_column_count(_pstmt);
	}
	
	int getDataCount() { /// Get data count.
		version (SQLITE_SAFE) _checkStatementAvailable();
		return sqlite3_data_count(_pstmt);
	}
	
	void isErr(int code, lazy char[] msg = ""){
		if( code ){
			if( available ){
				if( _conn ){
					if( _conn.available ){
						throw new SqliteException(_conn._pdb, _pstmt, code, msg);
					}
				}
				throw new SqliteException(_pstmt, code, msg);
			}
			throw new SqliteException(code, msg);
		}
	}

	
	static const int offset	= 1;
	void bindTuple(T...)(ref T t){
		version (SQLITE_SAFE) _checkStatementAvailable();
		int	code;	
		foreach(int i, v; T){
			static if( is ( v == float ) ){
				code	= sqlite3_bind_double(_pstmt, i + offset , t[i] );
			}else static if( is ( v == char[] ) ){
				code = sqlite3_bind_text(_pstmt, i +offset , t[i].ptr, t[i].length, SQLITE_TRANSIENT);
			}else static if( is ( v == wchar[] ) ){
				code = sqlite3_bind_text16(_pstmt, i +offset , t[i].ptr, t[i].length * 2, SQLITE_TRANSIENT);
			}else static if( is ( v == void[] ) ){
				code = sqlite3_bind_blob(_pstmt, i + offset , t[i].ptr, t[i].length, SQLITE_TRANSIENT);
			}else  static if( is ( v == int ) || is ( v == uint)  ){
				code	= sqlite3_bind_int(_pstmt, i +offset , t[i] );
			}else static if( is ( v == long ) ||  is ( v == ulong ) ){
				code	= sqlite3_bind_int64(_pstmt, i + offset , t[i] );
			}
			version (SQLITE_SAFE) {
				isErr(code, "bind index = " ~ ctfe2_i2a( i + offset) );
			}
		}
	}
	
	bool fetchTuple(T...)(ref T t){
		int code	= step();
		if( code != SQLITE_ROW ){
			if( code is SQLITE_DONE ){
				return false;
			}
			isErr(code);
		}
		foreach(int i, v; T){
			static if( is( v == int) || is( v == uint) ){
				t[i]	= sqlite3_column_int(_pstmt, i);
			}else static if( is( v == long) || is( v == ulong)){
				t[i]	= sqlite3_column_int64(_pstmt, i);
			}else static if( is( v == float ) ){
				t[i]	= sqlite3_column_double(_pstmt, i);
			}else static if( is( v == char[] ) ){
				t[i]	=  sqlite3_column_text(_pstmt, i)[0 .. sqlite3_column_bytes(_pstmt, i)].dup;
			}else static if( is( v == wchar[] ) ){
				t[i]	= (cast(wchar*) sqlite3_column_text16(_pstmt, i))[0 .. sqlite3_column_bytes16(_pstmt, i)/2 ].dup ;
			}else static if( is( v == void[] ) ){
				t[i]	= sqlite3_column_blob(_pstmt, i)[0 .. sqlite3_column_bytes(_pstmt, i)].dup ;
			}else{
				static assert(false);
			}
		}
		return true;
	}
}
